/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package io.github.jeddict.jcode.util;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <p>
 * API for performing inflections (pluralization, singularization, and so on) on
 * various strings. These inflections will be useful in code generators that
 * convert things like database table names into Java class names.</p>
 *
 * <p>
 * The <code>getInstance()</code> method returns a singleton instance of this
 * class with a default set of rules, which can then be customized. Rules added
 * during customization will take precedence over the standard ones. Use the
 * <code>addIrregular()</code>, <code>addPlural()</code>,
 * <code>addSingular()</code>, and <code>addUncountable()</code> methods to add
 * additional rules ot the default ones.</p>
 *
 * <p>
 * <strong>IMPLEMENTATION NOTE</strong> - The default implementation is intended
 * to be functionally compatible with the <code>Inflector::inflections</code>
 * class in Ruby on Rails. The <code>gsub()</code> method on Ruby strings
 * matches regular expressions anywhere in the input. However, nearly all of the
 * actual patterns used in this module use <code>$</code> at the end to match
 * the end of the input string (so that only the last word in a multiple word
 * phrase will be singularized or pluralized). Therefore, the Java versions of
 * the regular expressions have been modified to capture all text before the
 * interesting characters at the end, and emit them as part of the result, so
 * that the entire string can be matched against a pattern once.</p>
 */
public class Inflector {

    /**
     * <p>
     * Private constructor to avoid instantiation.</p>
     */
    private Inflector() {

        addPlural("$", "s", false);
        addPlural("(.*)$", "\\1s");
        addPlural("(.*)(ax|test)is$", "\\1\\2es");
        addPlural("(.*)(octop|vir)us$", "\\1\\2i");
        addPlural("(.*)(alias|status)$", "\\1\\2es");
        addPlural("(.*)(bu)s$", "\\1\\2ses");
        addPlural("(.*)(buffal|tomat)o$", "\\1\\2oes");
        addPlural("(.*)([ti])um$", "\\1\\2a");
        addPlural("(.*)sis$", "\\1ses");
        addPlural("(.*)(?:([^f])fe|([lr])f)$", "\\1\\3ves");
        addPlural("(.*)(hive)$", "\\1\\2s");
        addPlural("(.*)(tive)$", "\\1\\2s"); // Added for consistency with singular rules
        addPlural("(.*)([^aeiouy]|qu)y$", "\\1\\2ies");
        addPlural("(.*)(series)$", "\\1\\2"); // Added for consistency with singular rules
        addPlural("(.*)(movie)$", "\\1\\2s"); // Added for consistency with singular rules
        addPlural("(.*)(x|ch|ss|sh)$", "\\1\\2es");
        addPlural("(.*)(matr|vert|ind)ix|ex$", "\\1\\2ices");
        addPlural("(.*)(o)$", "\\1\\2es"); // Added for consistency with singular rules
        addPlural("(.*)(shoe)$", "\\1\\2s"); // Added for consistency with singular rules
        addPlural("(.*)([m|l])ouse$", "\\1\\2ice");
        addPlural("^(ox)$", "\\1en");
        addPlural("(.*)(vert|ind)ex$", "\\1\\2ices"); // Added for consistency with singular rules
        addPlural("(.*)(matr)ix$", "\\1\\2ices"); // Added for consistency with singular rules
        addPlural("(.*)(quiz)$", "\\1\\2zes");

        addSingular("(.*)s$", "\\1");
        addSingular("(.*)(n)ews$", "\\1\\2ews");
        addSingular("(.*)([ti])a$", "\\1\\2um");
        addSingular("(.*)((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$", "\\1\\2sis");
        addSingular("(.*)(^analy)ses$", "\\1\\2sis");
        addSingular("(.*)([^f])ves$", "\\1\\2fe");
        addSingular("(.*)(hive)s$", "\\1\\2");
        addSingular("(.*)(tive)s$", "\\1\\2");
        addSingular("(.*)([lr])ves$", "\\1\\2f");
        addSingular("(.*)([^aeiouy]|qu)ies$", "\\1\\2y");
        addSingular("(.*)(s)eries$", "\\1\\2eries");
        addSingular("(.*)(m)ovies$", "\\1\\2ovie");
        addSingular("(.*)(x|ch|ss|sh)es$", "\\1\\2");
        addSingular("(.*)([m|l])ice$", "\\1\\2ouse");
        addSingular("(.*)(bus)es$", "\\1\\2");
        addSingular("(.*)(o)es$", "\\1\\2");
        addSingular("(.*)(shoe)s$", "\\1\\2");
        addSingular("(.*)(cris|ax|test)es$", "\\1\\2is");
        addSingular("(.*)(octop|vir)i$", "\\1\\2us");
        addSingular("(.*)(alias|status)es$", "\\1\\2");
        addSingular("^(ox)en", "\\1");
        addSingular("(.*)(vert|ind)ices$", "\\1\\2ex");
        addSingular("(.*)(matr)ices$", "\\1\\2ix");
        addSingular("(.*)(quiz)zes$", "\\1\\2");

        addIrregular("child", "children");
        addIrregular("man", "men");
        addIrregular("move", "moves");
        addIrregular("person", "people");
        addIrregular("sex", "sexes");

        addUncountable("equipment");
        addUncountable("fish");
        addUncountable("information");
        addUncountable("money");
        addUncountable("rice");
        addUncountable("series");
        addUncountable("sheep");
        addUncountable("species");

    }

    // -------------------------------------------------------- Static Variables
    /**
     * <p>
     * The singleton instance returned by the default <code>getInstance()</code>
     * method.</p>
     */
    private transient static Inflector instance = null;

    /**
     * <p>
     * List of <code>Replacer</code>s for performing replacement operations on
     * matches for plural words.</p>
     */
    private List plurals = new LinkedList();

    /**
     * <p>
     * List of <code>Replacer</code>s for performing replacement operations on
     * matches for addSingular words.</p>
     */
    private List singulars = new ArrayList();

    /**
     * <p>
     * List of words that represent addUncountable concepts that cannot be
     * pluralized or singularized.</p>
     */
    private List uncountables = new LinkedList();

    // ------------------------------------------------------ Instance Variables
    // ---------------------------------------------------------- Static Methods
    /**
     * <p>
     * Return a fully configured {@link Inflector} instance that can be used for
     * performing transformations.</p>
     */
    public static Inflector getInstance() {

        if (instance == null) {
            instance = new Inflector();
        }
        return instance;

    }

    // ---------------------------------------------------------- Public Methods
    /**
     * <p>
     * Convert strings to <code>EmbeddedCamelCase</code>. Embedded underscores
     * will be removed.</p>
     *
     * @param word Word to be converted
     */
    public String camelize(String word) {

        return camelize(word, false);

    }

    /**
     * <p>
     * Convert word strings consisting of lower case letters and underscore
     * characters between words into <code>embeddedCamelCase</code> or
     * <code>EmbeddedCamelCase</code>, depending on the <code>lower</code> flag.
     * Embedded underscores will be removed. Embedded '/' characters will be
     * replaced by '.', making this method useful in converting path-like names
     * into fully qualified classnames.</p>
     *
     * <p>
     * <strong>IMPLEMENTATION DIFFERENCE</strong> - The Rails version of this
     * method also converts '/' characters to '::' because that reflects the
     * normal syntax for fully qualified names in Ruby.</p>
     *
     * <table border="1" width="100%">
     * <tr>
     * <th>Input</th>
     * <th>Output</th>
     * </tr>
     * <tr>
     * <td>"foo_bar", false</td>
     * <td>"FooBar"</td>
     * </tr>
     * <tr>
     * <td>"foo_bar", true</td>
     * <td>"fooBar"</td>
     * </tr>
     * <tr>
     * <td>"foo_bar/baz", false</td>
     * <td>"FooBar.Baz"</td>
     * </tr>
     * <tr>
     * <td>"foo_bar/baz", true</td>
     * <td>"fooBar.Baz"</td>
     * </tr>
     * </table>
     *
     * @param word Word to be converted
     * @param flag Flag indicating that the initial character should be lower
     * cased instead of upper cased
     */
    public String camelize(String word, boolean flag) {
        if (word.length() == 0) {
            return word;
        }

        StringBuffer sb = new StringBuffer(word.length());
        if (flag) {
            sb.append(Character.toLowerCase(word.charAt(0)));
        } else {
            sb.append(Character.toUpperCase(word.charAt(0)));
        }
        boolean capitalize = false;
        for (int i = 1; i < word.length(); i++) {
            char ch = word.charAt(i);
            if (capitalize) {
                sb.append(Character.toUpperCase(ch));
                capitalize = false;
            } else if (ch == '_') {
                capitalize = true;
            } else if (ch == '/') {
                capitalize = true;
                sb.append('.');
            } else {
                sb.append(ch);
            }
        }
        return sb.toString();

    }

    /**
     * <p>
     * Create and return a simple class name that corresponds to a addPlural
     * table name. Any leading schema name will be trimmed.</p>
     *
     * <table border="1" width="100%">
     * <tr>
     * <th>Input</th>
     * <th>Output</th>
     * </tr>
     * <tr>
     * <td>"foo_bars"</td>
     * <td>"FooBar"</td>
     * </tr>
     * <tr>
     * <td>"baz"</td>
     * <td>"Baz"</td>
     * </tr>
     * </table>
     *
     * @param tableName Table name to be converted
     */
    public String classify(String tableName) {

        int period = tableName.lastIndexOf('.');
        if (period >= 0) {
            tableName = tableName.substring(period + 1);
        }
        return camelize(singularize(tableName));

    }

    /**
     * <p>
     * Replace underscores in the specified word with dashes.</p>
     *
     * <table border="1" width="100%">
     * <tr>
     * <th>Input</th>
     * <th>Output</th>
     * </tr>
     * <tr>
     * <td>"foo_bar"</td>
     * <td>"foo-bar"</td>
     * </tr>
     * <tr>
     * <td>"baz"</td>
     * <td>"baz"</td>
     * </tr>
     * </table>
     *
     * @param word Word to be converted
     */
    public String dasherize(String word) {

        return word.replace('_', '-');

    }

    /**
     * <p>
     * Remove any package name from a fully qualified class name, returning only
     * the simple classname.</p>
     *
     * <table border="1" width="100%">
     * <tr>
     * <th>Input</th>
     * <th>Output</th>
     * </tr>
     * <tr>
     * <td>"java.util.Map"</td>
     * <td>"Map"</td>
     * </tr>
     * <tr>
     * <td>"String"</td>
     * <td>"String"</td>
     * </tr>
     * </table>
     *
     * @param className Fully qualified class name to be converted
     */
    public String demodulize(String className) {

        int period = className.lastIndexOf('.');
        if (period >= 0) {
            return className.substring(period + 1);
        } else {
            return className;
        }

    }

    /**
     * <p>
     * Create and return a foreign key name from a class name, separating the
     * "id" suffix with an underscore.</p>
     */
    public String foreignKey(String className) {

        return foreignKey(className, true);

    }

    /**
     * <p>
     * Create and return a foreign key name from a class name, optionally
     * inserting an underscore before the "id" portion.</p>
     *
     * <table border="1" width="100%">
     * <tr>
     * <th>Input</th>
     * <th>Output</th>
     * </tr>
     * <tr>
     * <td>"com.mymodel.Order", false</td>
     * <td>"orderid"</td>
     * </tr>
     * <tr>
     * <td>"com.mymodel.Order", true</td>
     * <td>"order_id"</td>
     * </tr>
     * <tr>
     * <td>"Message", false</td>
     * <td>"messageid"</td>
     * </tr>
     * <tr>
     * <td>"Message", true</td>
     * <td>"message_id"</td>
     * </tr>
     * </table>
     *
     * @param className Class name for which to create a foreign key
     * @param underscore Flag indicating whether an underscore should be emitted
     * between the class name and the "id" suffix
     */
    public String foreignKey(String className, boolean underscore) {

        return underscore(demodulize(className) + (underscore ? "_id" : "id"));

    }

    /**
     * <p>
     * Capitalize the first word in a lower cased and underscored string, turn
     * underscores into spaces, and string any trailing "_id". Like
     * <code>titleize()</code>, this is meant for creating pretty output, and is
     * not intended for code generation.</p>
     *
     * <table border="1" width="100%">
     * <tr>
     * <th>Input</th>
     * <th>Output</th>
     * </tr>
     * <tr>
     * <td>"employee_salary"</td>
     * <td>"Employee salary"</td>
     * </tr>
     * <tr>
     * <td>"author_id"</td>
     * <td>"Author"</td>
     * </tr>
     * </table>
     *
     * @param words Word string to be converted
     */
    public String humanize(String words) {

        if (words.endsWith("_id")) {
            words = words.substring(0, words.length() - 3);
        }
        StringBuffer sb = new StringBuffer(words.length());
        sb.append(Character.toUpperCase(words.charAt(0)));
        for (int i = 1; i < words.length(); i++) {
            char ch = words.charAt(i);
            if (ch == '_') {
                sb.append(' ');
            } else {
                sb.append(ch);
            }
        }
        return sb.toString();

    }

    /**
     * <p>
     * Turn a number into a corresponding ordinal string used to denote the
     * position in an ordered sequence.</p>
     *
     * <table border="1" width="100%">
     * <tr>
     * <th>Input</th>
     * <th>Output</th>
     * </tr>
     * <tr>
     * <td>1</td>
     * <td>"1st"</td>
     * </tr>
     * <tr>
     * <td>2</td>
     * <td>"2nd"</td>
     * </tr>
     * <tr>
     * <td>3</td>
     * <td>"3rd"</td>
     * </tr>
     * <tr>
     * <td>4</td>
     * <td>"rth"</td>
     * </tr>
     * <tr>
     * <td>1002</td>
     * <td>"1002nd"</td>
     * </tr>
     * <tr>
     * <td>2012</td>
     * <td>"2012th"</td>
     * </tr>
     * </table>
     *
     * @param number Number to be converted
     */
    public String ordinalize(int number) {

        int modulo = number % 100;
        if ((modulo >= 11) && (modulo <= 13)) {
            return "" + number + "th";
        }
        switch (number % 10) {
            case 1:
                return "" + number + "st";
            case 2:
                return "" + number + "nd";
            case 3:
                return "" + number + "rd";
            default:
                return "" + number + "th";
        }

    }

    /**
     * <p>
     * Return a addPlural version of the specified (addSingular) word.</p>
     *
     *
     * @param word Singular word to be converted
     */
    public String pluralize(String word) {

        // Scan uncountables and leave alone
        for (int i = 0; i < uncountables.size(); i++) {
            if (uncountables.get(i).equals(word)) {
                return word;
            }
        }

        // Scan our patterns for a match and return the correct replacement
        for (int i = 0; i < plurals.size(); i++) {
            Replacer replacer = (Replacer) plurals.get(i);
            if (replacer.matches(word)) {
                return replacer.replacement();
            }
        }

        // Return the original string unchanged
        return word;

    }

    /**
     * <p>
     * Return a addSingular version of the specified (addPlural) word.</p>
     *
     *
     * @param word Plural word to be converted
     */
    public String singularize(String word) {

        // Scan uncountables and leave alone
        for (int i = 0; i < uncountables.size(); i++) {
            if (uncountables.get(i).equals(word)) {
                return word;
            }
        }

        // Scan our patterns for a match and return the correct replacement
        for (int i = 0; i < singulars.size(); i++) {
            Replacer replacer = (Replacer) singulars.get(i);
            if (replacer.matches(word)) {
                return replacer.replacement();
            }
        }

        // Return the original string unchanged
        return word;

    }

    /**
     * <p>
     * Convert the simple name of a model class into the corresponding name of a
     * database table, by uncamelizing, inserting underscores, and pluralizing
     * the last word.</p>
     *
     * <table border="1" width="100%">
     * <tr>
     * <th>Input</th>
     * <th>Output</th>
     * </tr>
     * <tr>
     * <td>"RawScaledScorer"</td>
     * <td>"raw_scaled_scorers"</td>
     * </tr>
     * <tr>
     * <td>"fancyCategory"</td>
     * <td>"fancy_categories"</td>
     * </tr>
     * </table>
     *
     * @param className Class name to be converted
     */
    public String tableize(String className) {

        return pluralize(underscore(className));

    }

    /**
     * <p>
     * Capitalize all the words, and replace some characters in the string to
     * create a nicer looking title. This is meant for creating pretty output,
     * and is not intended for code generation.</p>
     *
     * <table border="1" width="100%">
     * <tr>
     * <th>Input</th>
     * <th>Output</th>
     * </tr>
     * <tr>
     * <td>"the honeymooners"</td>
     * <td>"The Honeymooners"</td>
     * </tr>
     * <tr>
     * <td>"x-men: the last stand"</td>
     * <td>"X Men: The Last Stand"</td>
     * </tr>
     * </table>
     *
     * @param words Word string to be converted
     */
    public String titleize(String words) {

        StringBuffer sb = new StringBuffer(words.length());
        boolean capitalize = true; // To get the first character right
        for (int i = 0; i < words.length(); i++) {
            char ch = words.charAt(i);
            if (Character.isWhitespace(ch)) {
                sb.append(' ');
                capitalize = true;
            } else if (ch == '-') {
                sb.append(' ');
                capitalize = true;
            } else if (capitalize) {
                sb.append(Character.toUpperCase(ch));
                capitalize = false;
            } else {
                sb.append(ch);
            }
        }
        return sb.toString();

    }

    /**
     * <p>
     * The reverse of <code>camelize()</code>, makes an underscored form from
     * the expression in the string. Changes "." to "/" to convert fully
     * qualified class names into paths.</p>
     *
     * <table border="1" width="100%">
     * <tr>
     * <th>Input</th>
     * <th>Output</th>
     * </tr>
     * <tr>
     * <td>"FooBar"</td>
     * <td>"foo_bar"</td>
     * </tr>
     * <tr>
     * <td>"fooBar"</td>
     * <td>"foo_bar"</td>
     * </tr>
     * <tr>
     * <td>"FooBar.Baz"</td>
     * <td>"foo_bar/baz"</td>
     * </tr>
     * <tr>
     * <td>"FooBar.Baz"</td>
     * <td>"foo_bar/baz"</td>
     * </tr>
     * </table>
     *
     * @param word Camel cased word to be converted
     */
    public String underscore(String word) {

        StringBuffer sb = new StringBuffer(word.length() + 5);
        boolean uncapitalize = false;
        for (int i = 0; i < word.length(); i++) {
            char ch = word.charAt(i);
            if (uncapitalize) {
                sb.append(Character.toLowerCase(ch));
                uncapitalize = false;
            } else if (ch == '.') {
                sb.append('/');
                uncapitalize = true;
            } else if (Character.isUpperCase(ch)) {
                if (i > 0) {
                    sb.append('_');
                }
                sb.append(Character.toLowerCase(ch));
            } else {
                sb.append(ch);
            }
        }
        return sb.toString();

    }

    // --------------------------------------------------- Customization Methods
    /**
     * <p>
     * Add the addSingular and addPlural forms of words that cannot be converted
     * using the normal rules.</p>
     *
     *
     * @param singular Singular form of the word
     * @param plural Plural form of the word
     */
    public void addIrregular(String singular, String plural) {

        addPlural("(.*)(" + singular.substring(0, 1) + ")" + singular.substring(1) + "$",
                "\\1\\2" + plural.substring(1));
        addSingular("(.*)(" + plural.substring(0, 1) + ")" + plural.substring(1) + "$",
                "\\1\\2" + singular.substring(1));

    }

    /**
     * <p>
     * Add a match pattern and replacement rule for converting addPlural forms
     * to addSingular forms. By default, matches will be case insensitive.</p>
     *
     *
     * @param match Match pattern regular expression
     * @param rule Replacement rule
     */
    public void addPlural(String match, String rule) {

        addPlural(match, rule, true);

    }

    /**
     * <p>
     * Add a match pattern and replacement rule for converting addPlural forms
     * to addSingular forms.</p>
     *
     *
     * @param match Match pattern regular expression
     * @param rule Replacement rule
     * @param insensitive Flag indicating this match should be case insensitive
     */
    public void addPlural(String match, String rule, boolean insensitive) {

        plurals.add(0, new Replacer(match, rule, insensitive));

    }

    /**
     * <p>
     * Add a match pattern and replacement rule for converting addSingular forms
     * to addPlural forms. By default, matches will be case insensitive.</p>
     *
     *
     * @param match Match pattern regular expression
     * @param rule Replacement rule
     */
    public void addSingular(String match, String rule) {

        addSingular(match, rule, true);

    }

    /**
     * <p>
     * Add a match pattern and replacement rule for converting addSingular forms
     * to addPlural forms.</p>
     *
     *
     * @param match Match pattern regular expression
     * @param rule Replacement rule
     * @param insensitive Flag indicating this match should be case insensitive
     */
    public void addSingular(String match, String rule, boolean insensitive) {

        singulars.add(0, new Replacer(match, rule, insensitive));

    }

    /**
     * <p>
     * Add a word that cannot be converted between addSingular and
     * addPlural.</p>
     *
     *
     * @param word Word to be added
     */
    public void addUncountable(String word) {

        uncountables.add(0, word.toLowerCase());

    }

    // --------------------------------------------------------- Private Classes
    /**
     * <p>
     * Internal class that uses a regular expression matcher to both match the
     * specified regular expression to a specified word, and (if successful)
     * perform the appropriate substitutions.</p>
     */
    private static class Replacer {

        // --------------------------------------------------------- Constructor
        public Replacer(String match, String rule, boolean insensitive) {

            pattern = Pattern.compile(match,
                    insensitive ? Pattern.CASE_INSENSITIVE : 0);
            this.rule = rule;

        }

        // -------------------------------------------------- Instance Variables
        private Matcher matcher = null;
        private Pattern pattern = null;
        private String rule = null;

        // ------------------------------------------------------ Public Methods
        /**
         * <p>
         * Return <code>true</code> if our regular expression pattern matches
         * the specified input. If it does, save necessary state information so
         * that the <code>replacement()</code> method will return appropriate
         * results based on the <code>rule</code> specified to our
         * constructor.</p>
         *
         * @param input Input characters to be matched
         */
        public boolean matches(String input) {

            matcher = pattern.matcher(input);
            if (matcher.matches()) {
                return true;
            } else {
                this.matcher = null;
                return false;
            }

        }

        /**
         * <p>
         * Return a replacement string based on the <code>rule</code> that was
         * specified to our constructor. This method <strong>MUST</strong>
         * only be called when the <code>matches()</code> method has returned
         * <code>true</code>.</p>
         */
        public String replacement() {

            StringBuffer sb = new StringBuffer();
            boolean group = false;
            for (int i = 0; i < rule.length(); i++) {
                char ch = rule.charAt(i);
                if (group) {
                    sb.append(matcher.group(Character.digit(ch, 10)));
                    group = false;
                } else if (ch == '\\') {
                    group = true;
                } else {
                    sb.append(ch);
                }
            }
            return sb.toString();

        }

    }

}
